I'm Terrence

Flutter 实战系列:await 在工程中实际应用的一些思考

0x01

这个系列有一段时间没更新了,最近在业务编码过程中,有一些想法,于是把其中的点滴记录下来,跟大家分享下。

0x02 callback

在客户端开发中,日常会遇到很多 callback, delegate 之类的回调,所以当转过来到 dart 开发也可能会理所当然的习以为常了。我们来看下项目中这种情况:

先以 OC 层的代码说一下上下文:
Thunder的进入房间是异步的,通过 joinRoom 触发,然后通过 ThunderEventDelegateonJoinRoomSuccess 来回调结果。

1
- (void)thunderEngine:(ThunderEngine *)engine onJoinRoomSuccess:(NSString *)room withUid:(NSString *)uid elapsed:(NSInteger)elapsed

同样,退房间的结果也是异步的,通过 leaveRoom 触发,然后通过 ThunderEventDelegateonLeaveRoomWithStats

1
- (void)thunderEngine:(ThunderEngine *)engine onLeaveRoomWithStats:(ThunderRtcRoomStats * _Nonnull)stats

无可厚非,在客户端开发中,这种 delegete 回调方式是很正常不过的,因为这个ThunderEventDelegate的存在, 与直接在函数里面加callback 相比,这样让业务层可以自主去赋值,自定义去对象去处理这些回调,给业务层提供解耦的机会,就不用把所有逻辑都堆在同一个文件里面。

以上角度是咱们在 OC 开发者的角度来看的。

继续以 Thunder 进退房间为例子,业务日常开发的逻辑,往往是实现比当初设计要复杂得多的,比如:业务存在某种场景,需要先退房间A,再进房间B。
由于退房间的过程是异步的,如果继续以客户端的思维去编码的话,就必须把进房间的流程,堆在 onLeaveRoomWithStats 退房间完成的回调里面了,而其他业务正常的退房间,又不用进房间的,于是乎,onLeaveRoomWithStats 里面的逻辑会慢慢多起来。

1
2
//退房间A
[self leaveRoom]
1
2
3
4
5
6
7
8
9
10
11
//回调
- (void)thunderEngine:(ThunderEngine *)engine onLeaveRoomWithStats:(ThunderRtcRoomStats * _Nonnull)stats
{
if (needJoinRoomB) {
//joinRoomB logic
[self joinRoom:B];
} else {
//log
// do nothing
}
}

当新人来读代码的时候,要了解 joinRoomB logic 的时候,又得跳到 onJoinRoomSuccess 看,在进入房间后到底做了什么

1
2
3
4
- (void)thunderEngine:(ThunderEngine *)engine onLeaveRoomWithStats:(ThunderRtcRoomStats * _Nonnull)stats {
//join Room success logic
[self openMic];
}

说实话,这样的代码阅读起来由于跨度比较大(累)的,逻辑分离的比较厉害。

当我们转到 dart 开发的时候,可以想一下,这种回调的方式是否是 dart 语言的最佳实践呢?

0x03 await kills callback

dart 因为 await 的存在,我们可以把很多回调式的写法,转换成流式。继续用刚刚进退房间那个做例子,来看看dart的写法可以简化到一种怎么样的程度:

1
2
3
4
5
6
7
8
//leaveRoom A
await koThunder.leaveRoom();

//joinRoom B
await koThunder.joinRoom(B);

//join Room success logic
openMic();

没错,就是这么简单,3行搞定···,业务上层完全脱离了回调来编程了,逻辑很紧凑,阅读起来会很舒服。

这个 koThunder 是我们业务对 FlutterThunder 的隔离层,在里面,我们对回调的方式进行了处理。
以 leaveRoom 为例吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//KoThunder 

Completer<bool> _leaveRoomCompleter;


Future<bool> leaveRoom() async {
ALog.info(_tag, "call leaveRoom");

_leaveRoomCompleter = Completer();
int res = await FlutterThunder.leaveRoom();
if (res != 0) {
ALog.info(_tag, "leaveRoom failed");
_leaveRoomCompleter.complete(false);
} else {
ALog.info(_tag, "wait leaveRoom callback");
}

return _leaveRoomCompleter.future;
}

@override
void onLeaveRoomWithStats() {
_leaveRoomCompleter.complete(true);
}

joinRoom 的处理操作也同理,这里不重复了。

原理很简单,无非就是用 Completer 来处理回调,向外统一暴露一个 Future 的东西就好了。这样写,个人觉得十分的 dart 化。把复杂留给自己,简单暴露给调用方,这样的代码,让业务上层的逻辑更容易阅读,更容易维护。

0x04 总结

我们在公司开发 flutter plugin 也有一段时间了,但很多时候都是按葫芦画瓢,对底层sdk 做一层dart层的封装, 能调用就完事了,并没有真正地把 dart 的语言特性发挥出来,这个是做得很不够的地方。唯有真正深入业务,理解业务的复杂,并把相关语言特性发挥出来,才能写出更好用的基础组件。